package com.rendevous.utils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.StringTokenizer;

/**
 * @author How Wei Keng A0087836
 * @version 0.1
 * @since 15-10-2012
 * 
 *        This class is the command and argument parser. Parse and use
 *        validators to check command and its argument's validity.
 * 
 *        Assumptions :-nil-
 * 
 */
public class CommandParser {

	private String parsedCommand;
	private HashMap<String, Object> parsedArgs;
	private String parserFeedback;
	private boolean isParsedStatus;

	public CommandParser() {
		this.parsedCommand = "";
		parsedArgs = new HashMap<String, Object>();
		parserFeedback = "Error in Parsing";
		isParsedStatus = false;
		initialiseArgs();
	}

	/**
	 * 
	 * Init. the map with pre-defined values
	 */
	private void initialiseArgs() {
		parsedArgs.put("content", (String) "");
		parsedArgs.put("startDate", (Date) null);
		parsedArgs.put("endDate", (Date) null);
		parsedArgs.put("tagsList", new ArrayList<String>());
		parsedArgs.put("priority", (String) "");
		parsedArgs.put("taskNum", -1);
	}

	/**
	 * 
	 * Parse the userInput into commands and Args parts
	 * 
	 * @param userInput
	 *            -The user's input string
	 * @see #extractAndCheckCommand(String)
	 * @see #extractAndCheckArgs(String)
	 */
	public void parseUserInput(String userInput) {
		boolean hasCorrectCommandType = extractAndCheckCommand(userInput);
		boolean hasCorrectArgsType = false;
		if (hasCorrectCommandType == true) {
			parseShortCutCommand();
			hasCorrectArgsType = extractAndCheckArgs(userInput);
		} else {
			this.parserFeedback = "Unknow Command.";
		}
		this.isParsedStatus = hasCorrectCommandType && hasCorrectArgsType;
	}

	/**
	 * 
	 * Parse the userInput into command part and check
	 * 
	 * @param userInput
	 *            -The user's input string
	 * @see Validator#isValidCommandType(String)
	 * @return hasValidCommandType
	 */
	private boolean extractAndCheckCommand(String userInput) {
		Validator cmdValidator = new Validator();
		boolean hasValidCommandType = false;
		this.parsedCommand = userInput.trim();
		int indexOfSpace = this.parsedCommand.indexOf(' ');
		if (indexOfSpace != -1) {
			this.parsedCommand = this.parsedCommand.substring(0, indexOfSpace);
		}
		hasValidCommandType = cmdValidator.isValidCommandType(this.parsedCommand);
		return hasValidCommandType;
	}

	/**
	 * 
	 * Parse the userInput into args part and check with respect to the command
	 * 
	 * @param userInput
	 *            -The user's input string
	 * @return hasValidArgs
	 */
	private boolean extractAndCheckArgs(String userInput) {
		boolean hasValidArgs = false;
		switch (this.parsedCommand) {
		case Constants.CMD_ADD:
			hasValidArgs = extractAndCheckAddTypeArgs(userInput);
			break;
		case Constants.CMD_UPDATE:
			hasValidArgs = extractAndCheckUpdateTypeArgs(userInput);
			break;
		case Constants.CMD_SEARCH:
			hasValidArgs = extractAndCheckSearchTypeArgs(userInput);
			break;
		case Constants.CMD_DO:
			hasValidArgs = extractAndCheckDoTypeArgs(userInput);
			break;
		case Constants.CMD_REMOVE:
			hasValidArgs = extractAndCheckRemoveTypeArgs(userInput);
			break;
		case Constants.CMD_LIST:
			hasValidArgs = extractAndCheckNoArgs(userInput);
			break;
		case Constants.CMD_LIST_BY_DATE:
			hasValidArgs = extractAndCheckNoArgs(userInput);
			break;
		case Constants.CMD_LIST_BY_PRIORITY:
			hasValidArgs = extractAndCheckNoArgs(userInput);
			break;
		case Constants.CMD_SWITCH:
			hasValidArgs = extractAndCheckNoArgs(userInput);
			break;
		case Constants.CMD_UNDO:
			hasValidArgs = extractAndCheckNoArgs(userInput);
			break;
		case Constants.CMD_HELP:
			hasValidArgs = extractAndCheckNoArgs(userInput);
			break;
		case Constants.CMD_EXIT:
			hasValidArgs = extractAndCheckNoArgs(userInput);
			break;
		default:
			break;
		}
		return hasValidArgs;
	}

	/**
	 * 
	 * Check for no args
	 * 
	 * @param userInput
	 *            -The user's input string
	 * @return hasValidNoArgs
	 */
	private boolean extractAndCheckNoArgs(String userInput) {
		boolean hasValidNoArgs = false;
		String argsString = userInput.substring(this.parsedCommand.length()).trim();
		if (argsString.length() == 0) {
			hasValidNoArgs = true;
		}
		return hasValidNoArgs;
	}

	/**
	 * 
	 * Check Remove-command's arg
	 * 
	 * @param userInput
	 *            -The user's input string
	 * @see #extractAndCheckTaskNum(String)
	 * @return hasValidRemoveTypeArgs
	 */
	private boolean extractAndCheckRemoveTypeArgs(String userInput) {
		boolean hasValidRemoveTypeArgs = false;
		String argsString = userInput.substring(this.parsedCommand.length()).trim();
		boolean hasValidTaskNum = extractAndCheckTaskNum(argsString);
		hasValidRemoveTypeArgs = hasValidTaskNum;
		return hasValidRemoveTypeArgs;
	}

	/**
	 * 
	 * Check Do-command's arg
	 * 
	 * @param userInput
	 *            -The user's input string
	 * @see #extractAndCheckTaskNum(String)
	 * @return hasValidDoTypeArgs
	 */
	private boolean extractAndCheckDoTypeArgs(String userInput) {
		boolean hasValidDoTypeArgs = false;
		String argsString = userInput.substring(this.parsedCommand.length()).trim();
		boolean hasValidTaskNum = extractAndCheckTaskNum(argsString);
		hasValidDoTypeArgs = hasValidTaskNum;
		return hasValidDoTypeArgs;
	}

	/**
	 * 
	 * Check Search-command's args
	 * 
	 * @param userInput
	 *            -The user's input string
	 * @see #extractTags(String)
	 * @see #extractAndCheckPriority(String)
	 * @return hasValidSearchTypeArgs
	 */
	private boolean extractAndCheckSearchTypeArgs(String userInput) {
		boolean hasValidSearchTypeArgs = false;
		String argsString = userInput.substring(this.parsedCommand.length()).trim();
		int indexOfTag = argsString.indexOf("@");
		int indexOfPriority = argsString.indexOf("#");
		boolean hasValidPriority = true;
		if (indexOfTag == -1 && indexOfPriority == -1
				&& argsString.length() >= 0) {
			parsedArgs.put("content", argsString);
		} else {
			if (indexOfTag != -1) {
				extractTags(argsString);
			}
			if (indexOfPriority != -1) {
				hasValidPriority = extractAndCheckPriority(argsString);
			}
		}
		hasValidSearchTypeArgs = hasValidPriority;
		return hasValidSearchTypeArgs;
	}

	/**
	 * 
	 * Check Update-command's args
	 * 
	 * @param userInput
	 *            -The user's input string
	 * @see #extractAndCheckTaskNum(String)
	 * @see #extractAndCheckContent(String)
	 * @see #extractAndCheckDates(String)
	 * @see #extractTags(String)
	 * @see #extractAndCheckPriority(String)
	 * @return hasValidUpdateTypeArgs
	 */
	private boolean extractAndCheckUpdateTypeArgs(String userInput) {
		boolean hasValidUpdateTypeArgs = false;
		String argsString = userInput.substring(this.parsedCommand.length()).trim();
		int indexOfQuote = argsString.indexOf("\"");
		int indexOfTag = argsString.indexOf("@");
		int indexOfDate = argsString.indexOf(">");
		int indexOfPriority = argsString.indexOf("#");
		boolean hasValidTaskNum = extractAndCheckTaskNum(argsString);
		boolean hasValidDates = true;
		boolean hasValidPriority = true;
		boolean hasValidContent = true;
		if (indexOfQuote == -1 && indexOfDate == -1 && indexOfTag == -1
				&& indexOfPriority == -1 && argsString.length() > 0) {
			this.parserFeedback = "Please check update syntax1.";
			hasValidUpdateTypeArgs = false;
			return hasValidUpdateTypeArgs;
		} else {
			if (indexOfQuote != -1) {
				hasValidContent = extractAndCheckContent(userInput);
			}
			if (indexOfDate != -1) {
				hasValidDates = extractAndCheckDates(argsString);
			}
			if (indexOfTag != -1) {
				extractTags(argsString);
			}
			if (indexOfPriority != -1) {
				hasValidPriority = extractAndCheckPriority(argsString);
			}
		}
		hasValidUpdateTypeArgs = hasValidTaskNum && hasValidContent
				&& hasValidDates && hasValidPriority;
		return hasValidUpdateTypeArgs;
	}

	/**
	 * 
	 * Check Add-command's args
	 * 
	 * @param userInput
	 *            -The user's input string
	 * @see #extractAndCheckContent(String)
	 * @see #extractAndCheckDates(String)
	 * @see #extractTags(String)
	 * @see #extractAndCheckPriority(String)
	 * @return hasValidAddTypeArgs
	 */
	private boolean extractAndCheckAddTypeArgs(String userInput) {
		boolean hasValidAddTypeArgs = false;
		boolean hasValidDates = true;
		boolean hasValidPriority = true;
		boolean hasValidContent = extractAndCheckContent(userInput);
		String argsString = userInput.substring(this.parsedCommand.length()).trim();
		int indexOfTag = argsString.indexOf("@");
		int indexOfDate = argsString.indexOf(">");
		int indexOfPriority = argsString.indexOf("#");

		if (indexOfDate == -1 && indexOfTag == -1 && indexOfPriority == -1
				&& argsString.length() > 0) {
			this.parserFeedback = "Please check add syntax1.";
			hasValidAddTypeArgs = false;
			return hasValidAddTypeArgs;
		} else {
			if (hasValidContent && indexOfDate != -1) {
				hasValidDates = extractAndCheckDates(argsString);
			}
			if (hasValidContent && indexOfTag != -1) {
				extractTags(argsString);
			}
			if (hasValidContent && indexOfPriority != -1) {
				hasValidPriority = extractAndCheckPriority(argsString);
			}
		}
		hasValidAddTypeArgs = hasValidContent && hasValidDates
				&& hasValidPriority;
		return hasValidAddTypeArgs;
	}

	/**
	 * 
	 * Parse and Check task number validity
	 * 
	 * @param userInput
	 *            -The user's input string
	 * @see Validator#isCorrectNumberFormat(String)
	 * @return hasValidTaskNum
	 */
	private boolean extractAndCheckTaskNum(String argsString) {
		boolean hasValidTaskNum = false;
		int indexOfQuote = argsString.indexOf("\"");
		int indexOfTag = argsString.indexOf("@");
		int indexOfDate = argsString.indexOf(">");
		int indexOfPriority = argsString.indexOf("#");
		int minIndex = argsString.length();
		ArrayList<Integer> minIndexList = new ArrayList<Integer>();
		minIndexList.add(indexOfQuote);
		minIndexList.add(indexOfTag);
		minIndexList.add(indexOfDate);
		minIndexList.add(indexOfPriority);
		Collections.sort(minIndexList);
		for (Integer i : minIndexList) {
			if (i != -1) {
				minIndex = i;
				break;
			}
		}
		String taskNumStr = argsString.substring(0, minIndex).trim();

		Validator taskNumValidator = new Validator();
		hasValidTaskNum = taskNumValidator.isCorrectNumberFormat(taskNumStr);
		if (hasValidTaskNum) {
			parsedArgs.put("taskNum", Integer.parseInt(taskNumStr));
		}
		return hasValidTaskNum;
	}

	/**
	 * 
	 * Parse and check Priority-type args
	 * 
	 * @param argsString
	 *            -The arg's parts string
	 * @see Validator#hasValidPriority(String)
	 * @return hasValidPriority
	 */
	private boolean extractAndCheckPriority(String argsString) {
		int indexOfPriority = argsString.indexOf('#') + 1;
		String trimmedString = argsString.substring(indexOfPriority).trim();
		Validator priorityValidator = new Validator();
		boolean hasValidPriority = priorityValidator
				.hasValidPriority(trimmedString);

		if (hasValidPriority) {
			parsedArgs.put("priority", trimmedString);
		}

		return hasValidPriority;
	}

	/**
	 * 
	 * Parse and extract all Tag-type args
	 * 
	 * @param argsString
	 *            -The arg's parts string
	 */
	private void extractTags(String argsString) {
		int indexOfPriority = argsString.indexOf('#');
		int indexOfTag = argsString.indexOf('@');
		String trimmedString = argsString;
		if (indexOfPriority != -1) {
			trimmedString = argsString.substring(indexOfTag, indexOfPriority);
		}
		StringTokenizer st = new StringTokenizer(trimmedString, "@");
		ArrayList<String> tagList = new ArrayList<String>();
		while (st.hasMoreTokens()) {
			tagList.add(st.nextToken());
		}
		parsedArgs.put("tagsList", tagList);
	}

	/**
	 * 
	 * Parse and check Date-type args
	 * 
	 * @param argsString
	 *            -The arg's parts string
	 * @see #extractAndCheckDatesHelper(String)
	 * @return hasValidDateArgs
	 */
	private boolean extractAndCheckDates(String argsString) {
		boolean hasValidDateArgs = false;
		int indexOfDateStart = argsString.indexOf('>') + 1;
		String trimmedDateString = argsString.substring(indexOfDateStart);
		int indexOfTag = trimmedDateString.indexOf('@');
		int indexOfPriority = trimmedDateString.indexOf('#');
		try {
			if (indexOfTag == -1 && indexOfPriority == -1) {
				hasValidDateArgs = extractAndCheckDatesHelper(trimmedDateString);
			} else if (indexOfTag != -1) {
				trimmedDateString = trimmedDateString.substring(0, indexOfTag);
				hasValidDateArgs = extractAndCheckDatesHelper(trimmedDateString);
			} else {
				trimmedDateString = trimmedDateString.substring(0,
						indexOfPriority);
				hasValidDateArgs = extractAndCheckDatesHelper(trimmedDateString);
			}
		} catch (ParseException e) {
			this.parserFeedback = "Please check dates syntax2.";
			hasValidDateArgs = false;
		}
		return hasValidDateArgs;
	}
	/**
	 * 
	 * Helper method to Parse and check Date-type args
	 * 
	 * @param trimmedDateString
	 *            -The trimmed date's parts string
	 * @see #extractAndCheckEndDates(String)
	 * @see #extractAndCheckStartAndEndDates(String)
	 * @return hasValidDateArgs
	 */
	private boolean extractAndCheckDatesHelper(String trimmedDateString)
			throws ParseException {
		Validator dateValidator = new Validator();
		StringTokenizer st = new StringTokenizer(trimmedDateString, " ");
		int numOfTokens = st.countTokens();
		boolean hasValidDateArgs = false;
		if (numOfTokens == 2) {
			hasValidDateArgs = extractAndCheckEndDates(dateValidator, st,
					hasValidDateArgs);
		} else if (numOfTokens == 4) {
			hasValidDateArgs = extractAndCheckStartAndEndDates(dateValidator,
					st, hasValidDateArgs);
		} else {
			this.parserFeedback = "Please check dates syntax3.";
			hasValidDateArgs = false;
		}
		return hasValidDateArgs;
	}
	/**
	 * 
	 * Helper method to Parse and check END-Date-type args
	 * 
	 * @param trimmedDateString
	 *            -The trimmed date's parts string
	 * @see Validator#isCorrectDateTimeSyntax(Date,Date)
	 * @return hasValidDateArgs
	 */
	private boolean extractAndCheckEndDates(Validator dateValidator,
			StringTokenizer st, boolean hasValidDateArgs) throws ParseException {
		String endDateString;
		boolean hasCorrectDatesSyntax;
		Date startDate = new Date();
		Date endDate;
		endDateString = st.nextToken() + " " + st.nextToken();
		endDate = new SimpleDateFormat(Constants.DATE_FORMAT)
				.parse(endDateString);
		hasCorrectDatesSyntax = dateValidator.isCorrectDateTimeSyntax(startDate,
				endDate);
		if (hasCorrectDatesSyntax) {
			parsedArgs.put("endDate", (Date) endDate);
			hasValidDateArgs = true;
		} else {
			this.parserFeedback = "Please input a future date and time.";
			hasValidDateArgs = false;
		}
		return hasValidDateArgs;
	}
	/**
	 * 
	 * Helper method to Parse and check START-Date-type and END-Date-type args
	 * 
	 * @param trimmedDateString
	 *            -The trimmed date's parts string
	 * @see Validator#isCorrectDateTimeSyntax(Date,Date)
	 * @return hasValidDateArgs
	 */
	private boolean extractAndCheckStartAndEndDates(Validator dateValidator,
			StringTokenizer st, boolean hasValidDateArgs) throws ParseException {
		String endDateString;
		String startDateString;
		boolean hasCorrectDatesSyntax;
		Date startDate;
		Date endDate;
		startDateString = st.nextToken() + " " + st.nextToken();
		endDateString = st.nextToken() + " " + st.nextToken();
		startDate = new SimpleDateFormat(Constants.DATE_FORMAT)
				.parse(startDateString);
		endDate = new SimpleDateFormat(Constants.DATE_FORMAT)
				.parse(endDateString);
		hasCorrectDatesSyntax = dateValidator.isCorrectDateTimeSyntax(startDate,
				endDate);
		if (hasCorrectDatesSyntax) {
			parsedArgs.put("startDate", new SimpleDateFormat(Constants.DATE_FORMAT)
					.parse(startDateString));
			parsedArgs.put("endDate", new SimpleDateFormat(Constants.DATE_FORMAT)
					.parse(endDateString));
			hasValidDateArgs = true;
		} else {
			this.parserFeedback = "Please check start and end date/time.";
			hasValidDateArgs = false;
		}
		return hasValidDateArgs;
	}
	/**
	 * 
	 * Parse and check task's content-type args
	 * 
	 * @param argsString
	 *            -The trimmed date's parts string
	 * @return hasValidContentArg
	 */
	private boolean extractAndCheckContent(String argsString) {
		boolean hasValidContentArg = false;
		int indexOfOpenQuote = argsString.indexOf("\"");
		int indexOfCloseQUote = argsString.lastIndexOf("\"");
		if (indexOfOpenQuote != indexOfCloseQUote) {
			String taskContent = argsString.substring(indexOfOpenQuote + 1,
					indexOfCloseQUote).trim();
			if (taskContent.length() > 0) {
				parsedArgs.put("content", taskContent);
				hasValidContentArg = true;
			} else {
				this.parserFeedback = "Please type in a task content.";
			}
		} else {
			this.parserFeedback = "Please check add content syntax.";
		}
		return hasValidContentArg;
	}
	/**
	 * 
	 * Parse shortcut commands to its actual command type
	 */
	private void parseShortCutCommand() {
		switch (parsedCommand.toLowerCase()) {
		case Constants.CMD_SHORTCUT_ADD:
			this.parsedCommand = Constants.CMD_ADD;
			break;
		case Constants.CMD_SHORTCUT_REMOVE:
			this.parsedCommand = Constants.CMD_REMOVE;
			break;
		case Constants.CMD_SHORTCUT_UPDATE:
			this.parsedCommand = Constants.CMD_UPDATE;
			break;
		case Constants.CMD_SHORTCUT_DO:
			this.parsedCommand = Constants.CMD_DO;
			break;
		case Constants.CMD_SHORTCUT_HELP:
			this.parsedCommand = Constants.CMD_HELP;
			break;
		case Constants.CMD_SHORTCUT_UNDO:
			this.parsedCommand = Constants.CMD_UNDO;
			break;
		case Constants.CMD_SHORTCUT_EXIT:
			this.parsedCommand = Constants.CMD_EXIT;
			break;
		case Constants.CMD_SHORTCUT_SWITCH:
			this.parsedCommand = Constants.CMD_SWITCH;
			break;
		default:
			break;
		}
	}

	public String getParsedCommand() {
		return this.parsedCommand;
	}

	public HashMap<String, Object> getParsedArgs() {
		return this.parsedArgs;
	}

	public boolean getIsParsedStatus() {
		return this.isParsedStatus;
	}

	public String getParserFeedback() {
		return this.parserFeedback;
	}
}
